/*
 * Copyright 2017 - 2024 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */

package infra.expression.spel.ast;

import java.lang.reflect.Modifier;
import java.util.function.Supplier;

import infra.bytecode.MethodVisitor;
import infra.bytecode.core.CodeFlow;
import infra.expression.EvaluationContext;
import infra.expression.EvaluationException;
import infra.expression.TypedValue;
import infra.expression.spel.ExpressionState;
import infra.expression.spel.SpelEvaluationException;
import infra.lang.Nullable;

/**
 * Represents a variable reference &mdash; for example, {@code #someVar}.
 *
 * @author Andy Clement
 * @author Sam Brannen
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
 * @since 4.0
 */
public class VariableReference extends SpelNodeImpl {

  /** Currently active context object. */
  private static final String THIS = "this";

  /** Root context object. */
  private static final String ROOT = "root";

  private final String name;

  public VariableReference(String variableName, int startPos, int endPos) {
    super(startPos, endPos);
    this.name = variableName;
  }

  @Override
  public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException {
    if (THIS.equals(this.name)) {
      return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this);
    }
    if (ROOT.equals(this.name)) {
      return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this);
    }
    TypedValue result = state.lookupVariable(this.name);
    // A null value in the returned VariableRef will mean either the value was
    // null or the variable was not found.
    return new VariableRef(this.name, result, state.getEvaluationContext());
  }

  @Override
  public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException {
    TypedValue result;
    if (THIS.equals(this.name)) {
      result = state.getActiveContextObject();
      // If the active context object (#this) is not the root context object (#root),
      // that means that #this is being evaluated within a nested scope (for example,
      // collection selection or collection project), which is not a compilable
      // expression, so we return the result without setting the exit type descriptor.
      if (result != state.getRootContextObject()) {
        return result;
      }
    }
    else if (ROOT.equals(this.name)) {
      result = state.getRootContextObject();
    }
    else {
      result = state.lookupVariable(this.name);
    }
    setExitTypeDescriptor(result.getValue());

    // A null value in the returned TypedValue will mean either the value was
    // null or the variable was not found.
    return result;
  }

  /**
   * Set the exit type descriptor for the supplied value.
   * <p>If the value is {@code null}, we set the exit type descriptor to
   * {@link Object}.
   * <p>If the value's type is not public, {@link #generateCode} would insert
   * a checkcast to the non-public type in the generated byte code which would
   * result in an {@link IllegalAccessError} when the compiled byte code is
   * invoked. Thus, as a preventative measure, we set the exit type descriptor
   * to {@code Object} in such cases. If resorting to {@code Object} is not
   * sufficient, we could consider traversing the hierarchy to find the first
   * public type.
   */
  private void setExitTypeDescriptor(@Nullable Object value) {
    if (value == null || !Modifier.isPublic(value.getClass().getModifiers())) {
      this.exitTypeDescriptor = "Ljava/lang/Object";
    }
    else {
      this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(value);
    }
  }

  @Override
  public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
          throws EvaluationException {

    return state.assignVariable(this.name, valueSupplier);
  }

  @Override
  public String toStringAST() {
    return "#" + this.name;
  }

  @Override
  public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException {
    return !(THIS.equals(this.name) || ROOT.equals(this.name));
  }

  @Override
  public boolean isCompilable() {
    return (this.exitTypeDescriptor != null);
  }

  @Override
  public void generateCode(MethodVisitor mv, CodeFlow cf) {
    if (THIS.equals(this.name) || ROOT.equals(this.name)) {
      mv.visitVarInsn(ALOAD, 1);
    }
    else {
      mv.visitVarInsn(ALOAD, 2);
      mv.visitLdcInsn(this.name);
      mv.visitMethodInsn(INVOKEINTERFACE, "infra/expression/EvaluationContext",
              "lookupVariable", "(Ljava/lang/String;)Ljava/lang/Object;", true);
    }
    CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor);
    cf.pushDescriptor(this.exitTypeDescriptor);
  }

  private static class VariableRef implements ValueRef {

    private final String name;

    private final TypedValue value;

    private final EvaluationContext evaluationContext;

    public VariableRef(String name, TypedValue value, EvaluationContext evaluationContext) {
      this.name = name;
      this.value = value;
      this.evaluationContext = evaluationContext;
    }

    @Override
    public TypedValue getValue() {
      return this.value;
    }

    @Override
    public void setValue(@Nullable Object newValue) {
      this.evaluationContext.setVariable(this.name, newValue);
    }

    @Override
    public boolean isWritable() {
      return true;
    }
  }

}
